iOS - CocoaPods 原理及库制作

CocoaPods 可以说是 iOS 开发应用最广泛的包管理工具,本篇文章主要介绍 CocoaPods 的第三方库是怎样从网络集成到我们本地的项目当中,也是制作私有库、开源库和 iOS 项目组件化的一个知识铺垫。

让我们从一张图片开始:

CocoaPods 工作流程
未命名文件

远程索引库

远程索引库里存放的是各种框架的描述信息,这个库托管在 Github 上,地址如下:

https://github.com/CocoaPods/Specs

每个框架下有数个版本,每个版本有一个 json 格式的描述信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "CAIStatusBar",
"version": "0.0.1",
"summary": "A simple indicator",
"homepage": "https://github.com/apple5566/CAIStatusBar.git",
"license": "MIT",
"authors": {
"apple5566": "zaijiank110@sohu.com"
},
"platforms": {
"ios": "6.0"
},
"source": {
"git": "https://github.com/apple5566/CAIStatusBar.git",
"tag": "0.0.1"
},
"source_files": "CAIStatusBar/**/*.{h,m}",
"resources": "CAIStatusBar/CAIStatusBar.bundle",
"requires_arc": true
}

其中 git 字段表示该框架的托管地址,也就是上面时序图中的 远程框架库

本地索引库

install cocoapods 命令后,需要执行 pod setup 这个命令,pod setup 命令就是将远程索引库克隆到本地来,本地索引库的路径如下:

1
~/.cocoapods/repos/master

本地索引库和远程索引库的目录一致,结构如下:

本地索引库
本地索引库

本地索引文件

当执行 pod search 命令时,如果本地索引文件不存在,会创建这个文件。

1
2
tianziyaodeMacBook-Air:~ Tian$ pod search afn
Creating search index for spec repo 'master'..

如果这个文件存在,则会在此文件中进行索引,确认所需要的框架是否存在,本地索引文件的路径如下:

1
~/资源库/Caches/CocoaPods

制作 CocoaPods 库

上面的流程清楚以后,制作 CocoaPods 库相信应该不会太难了,大致分为以下几步:

  1. 托管框架源码到 Git;
  2. 创建框架描述信息;
  3. 上传框架描述信息到 https://github.com/CocoaPods/Specs
  4. 命令行 pod setup , 创建本地索引库;
  5. 命令行 pod install ,将框架集成到项目中;

现在开始动手吧!首先在桌面新建一个 testLib 目录,在该目录下新建一个 Classes 目录,用来存放框架源码,然后将 testLib 托管到 Git。

你可以给 Classes 目录任意的命名,Classes 只是一种约定俗称的命名。

pod spec

pod spec 命令用于创建框架的描述信息文件,文档如下:

https://guides.cocoapods.org/syntax/podspec.html

现在在 testLib 目录下执行:

1
pod spec create testLib

目录下会创建一个 testLib.podspec 文件,然后编辑这个文件,主要有以下几个字段:

  • version:这个 spec 映射的版本,保证 Git 的 releases 与此对应;
  • homepage:项目主页;
  • source:框架源代码的托管地址;
  • tag:与 version 对应;
  • source_files:框架源代码的目录、文件、文件类型等规则;

CocoaPods 公开库

根据上面的步骤,现在你需要将生成的 testLib.podspec 文件上传到远程索引库,在此之前,你需要注册一个 Trunk 账号,文档如下:

https://guides.cocoapods.org/making/getting-setup-with-trunk.html

现在执行下面的命令,记得修改邮箱昵称描述等:

1
pod trunk register ziyao.tian@gmail.com 'Tian' --description='macbook air'

你的邮箱会收到一封邮件,打开邮件里面的链接,会有类似 you can back termainal 的提示,现在回到终端。

1
pod lib lint

检查 testLib.podspec 的合法性,根据错误提示修复问题,当显示 passed validation 后,执行下面的命令:

1
pod trunk push testLib.podspec

提示信息如下:

1
2
3
4
5
6
7
8
9
10
Updating spec repo `master`
--------------------------------------------------------------------------------
🎉 Congrats
🚀 testLib (0.0.7) successfully published
📅 October 17th, 00:38
🌎 https://cocoapods.org/pods/testLib
👍 Tell your friends!
--------------------------------------------------------------------------------

此时你的 testLib.podspec 就会 pull request 到远程索引库,CocoaPods 官方审核通过后,就可以出现在远程索引库中,当远程索引库收录后:

1
pod setup

这时你的本地索引库,会新加入 testLib.podspec 这条记录,但是本地索引文件还未更新,因此删除掉以下路径的本地索引文件:

1
~/资源库/Caches/CocoaPods/search_index.json

执行 pod search testLib 命令,当 search_index.json 文件重建完毕后,就可以在使用这个远程框架库了。

CocoaPods 私有库

有了公开库,当然也就有私有库,私有库主要分为远程和本地两种,什么时候会用到私用库呢?也就是需要将源码封装成库,但又不希望将源码公开,一般的使用场景是公司内部的组件化开发。

本地私有库

本地私有库就是创建一个仓库,将其存储在本地,在本地的其他工程中直接使用。首先在桌面新建一个库,路径如下:

1
LocalLib/NetWork/Classes/Test.swift

接着创建一个壳工程,现在你的目标是使用 pod 的方式,将 NetWork 这个库集成到壳工程中。

创建本地 GIt 仓库

NetWork 加入到 Git,命令如下:

1
2
3
git init
git add.
git commit -m 'x'

创建库描述文件

和公开库一样,我们需要先创建一个 spec 文件,命令如下:

1
pod spec create LocalLib

编辑 NetWork.podspec 文件,修改成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
Pod::Spec.new do |s|
s.name = "NetWork"
s.version = "0.0.1"
s.summary = "A short description of NetWork."
s.description = "A short description of NetWork.xxxxxxxxxxxxxxxxxx"
s.homepage = "http://EXAMPLE/NetWork"
s.license = "MIT"
s.author = { "tianziyao" => "ziyao.tian@gmail.com" }
s.source = { :git => "", :tag => "#{s.version}" }
s.source_files = "Classes", "Classes/**/*.{h,m,swift}"
end

现在你的本地库已经准备完毕了,下面就可以使用这个库了。

导入本地私有库

现在进入到壳工程目录下,执行命令:

1
pod init

编辑 Podfile 文件,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
target 'Test' do
use_frameworks!
pod 'NetWork', :path => '../NetWork'
target 'TestTests' do
inherit! :search_paths
end
target 'TestUITests' do
inherit! :search_paths
end
end

这里有一个 path 关键字,它表示在 pod install 执行时,在指定的路径下寻找 NetWork.podspec 文件。

下面执行 pod install 命令,提示信息如下:

1
2
3
4
5
6
Analyzing dependencies
Fetching podspec for `NetWork` from `../NetWork`
Downloading dependencies
Installing NetWork (0.0.1)
Generating Pods project
Integrating client project

现在 NetWork 这个库就集成到了壳工程中。

与使用远程库不同,本地库的源文件会在 Development Pods 这个目录下,而不是 Pods 目录,顺便一提,CocoaPods 的库开发,一般也是这样搭建环境的,开发完成后再修改 spec 文件,将其 pull request 到远程索引库。

CocoaPods 模板库

本地私有库这个方式还存在以下问题:

  • 需要手动创建 podspec 文件;
  • 无法单独测试,需要依托于壳工程运行;

假设我们有一个基础组件,里面全部是扩展文件,无法单独运行,如果依托壳工程运行,只有这一个组件,那么这个壳工程实际跟测试工程是一样的,但壳工程内有多个组件呢?

我们在壳工程中进行测试的话,不但要对其他的组件进行编译,而且自己负责的组件也可能会收到其他组件的影响,这样也就失去了组件化开发的本意,那么怎么优化呢?

单独测试

首先在 LocalLib/NetWork/ 路径下创建一个测试工程 Example,然后将 Classes 拖到这个测试工程中,这里需要注意的是,ExampleClasses 是引用关系,不要 Copy。

简单粗暴的拖拽,现在 Example 工程就可以使用 NetWork 库了。

另外一种方式是将 NetWork 通过 CocoaPods 安装在 Example 中,和安装在壳工程一样。

看到这里,是不是感觉很烦?就是想做个测试而已,还要拖来拖去,那么繁琐。

不要着急下面来介绍一种更快捷高效的方式,执行下面的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
pod lib create BaseMoudle
////////////////////////////////////////////////////////////////////////
What language do you want to use?? [ Swift / ObjC ]
> Swift
Would you like to include a demo application with your library? [ Yes / No ]
> Yes
Which testing frameworks will you use? [ Quick / None ]
> None
Would you like to do view based testing? [ Yes / No ]
> Yes

现在我们就有了一个 CocoaPods 的模板工程,它的结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── BaseMoudle
│   ├── Assets
│   └── Classes
│   └── ReplaceMe.swift
├── BaseMoudle.podspec
├── Example
│   ├── BaseMoudle
│   ├── BaseMoudle.xcodeproj
│   ├── BaseMoudle.xcworkspace
│   ├── Podfile
│   ├── Podfile.lock
│   ├── Pods
├── LICENSE
├── README.md
└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj

看吧,把源码拖到 ReplaceMe.swift 的同级目录,执行 pod install,就完成了本地私有库和其测试工程。

这一步可能会有 Swift 语言版本的问题,保持测试工程和私有库源码语言版本一致就可以。

远程私有库

远程私有库工作流程
远程私有库

现在使用 pod lib create 就可以方便的生成一个本地私有库了,但是本地私有库有一定的局限性,例如:

  • 需要在 Podfile 文件中主动指明路径;
  • 版本升级不容易维护;
  • 多人开发时,不方便进行合作;

远程私有库就可以方便的解决以上的问题,制作远程私有库分为以下几个步骤:

  1. 创建私有 Git 远程仓库;
  2. 创建私有 CocoaPods 远程索引库;
  3. 创建 Pod 所需要的项目工程文件,并上传到 Git 远程私有库;
  4. 验证 podspec 描述文件;
  5. 向私有 CocoaPods 远程索引库提交 podspec 描述文件;
  6. 使用 Pod 库;

Git 仓库的创建在此就不在赘述了,本文中我使用码市做示例,私有 CocoaPods 远程索引库实际上也是一个 Git 仓库,现在我们有两个私有库,一个用来存放 Pod 库的源码,一个用来存放 Pod 库的描述文件。

SSH 授权

添加私有索引库需要使用 SSH 授权,也是和 Git 仓库一样的,了解的同学可以跳过这一步骤,首先创建公钥:

1
ssh-keygen

然后找到下面的文件:

1
~/.ssh/id_rsa.pub

里面存放的字符就是公钥了,然后将公钥添加码市,链接如下:

1
https://coding.net/user/account/setting/keys

添加私有远程索引库

现在执行 pod repo,可以看到下面的信息:

1
2
3
4
master
- Type: git (master)
- URL: https://github.com/CocoaPods/Specs.git
- Path: /Users/Tian/.cocoapods/repos/master

现在我们只有一个 CocoaPods 远程索引库,也是官方的索引库,下面执行:

1
pod repo add TZYSpecs git@git.coding.net:tianziyao/TZYSpecs.git

此时我们的 CocoaPods 远程索引库就安装好了,到下面的路径去看一下:

1
~/.cocoapods/repos

上传源码到 Git

还记得 pod lib create 命令吗?前面我们使用它来制作了本地私有库,现在它又排上用场了,执行:

1
pod lib create BaseComponent

源码拖到 ReplaceMe.swift 的同级目录,它现在看起来应该是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── BaseComponent
│   ├── Assets
│   └── Classes
│   ├── Extension
│   │   ├── Array+Safe.swift
│   │   ├── CALayer+PauseAimate.swift
│   │   ├── UIImage+.swift
│   │   └── UIView+Property.swift
├── BaseComponent.podspec
├── Example
├── LICENSE
├── README.md
└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj

执行 pod install,就完成了本地私有库和其测试工程,通过测试之后,我们就可以把这个本地私有库制作成远程私有库了。

首先修改 BaseComponent.podspec 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Pod::Spec.new do |s|
s.name = 'BaseComponent'
s.version = '0.1.0'
s.summary = '基础组价'
s.description = '包括基本配置,常量,扩展,工具类等'
s.homepage = 'https://coding.net/u/tianziyao/p/BaseComponent'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'tianziyao' => 'ziyao.tian@gmail.com' }
s.source = { :git => 'https://git.coding.net/tianziyao/BaseComponent.git', :tag => s.version.to_s }
s.ios.deployment_target = '8.0'
s.source_files = 'BaseComponent/Classes/**/*'
end

然后使用质量检查工具验证一下,保证在 BaseComponent.podspec 路径下,执行:

1
pod lib lint

如果你使用 Swift,会得到一个提示:

1
2
3
4
5
6
-> BaseComponent (0.1.0)
- WARN | [iOS] swift_version: The validator for Swift projects uses Swift 3.0 by default, if you are using a different version of swift you can use a `.swift-version` file to set the version for your Pod. For example to use Swift 2.3, run:
`echo "2.3" > .swift-version`
[!] BaseComponent did not pass validation, due to 1 warning (but you can use `--allow-warnings` to ignore it).
You can use the `--no-clean` option to inspect any issue.

根据提示修复就好了,在这里你可能会遇到很多 Swift 语言版本的问题,善用搜索引擎吧,通过检验以后提示如下:

1
2
3
-> BaseComponent (0.1.0)
BaseComponent passed validation.

下面执行:

1
2
git add .
git commit -m 'x'

然后和远程仓库进行关联:

1
2
3
git remote add origin https://git.coding.net/tianziyao/BaseComponent.git
git pull origin master
git push origin master

上传 Spec 到远程索引库

首先执行下面的命令:

1
pod spec lint

提示如下:

1
2
3
4
5
6
7
8
9
10
11
-> BaseComponent (0.1.0)
- ERROR | [iOS] unknown: Encountered an unknown error ([!] /usr/local/bin/git clone https://git.coding.net/tianziyao/BaseComponent.git /var/folders/2v/qkx5m4sx4dg86x4c82yfyjdc0000gn/T/d20171021-69604-1bekfgk --template= --single-branch --depth 1 --branch 0.1.0
Cloning into '/var/folders/2v/qkx5m4sx4dg86x4c82yfyjdc0000gn/T/d20171021-69604-1bekfgk'...
warning: Could not find remote branch 0.1.0 to clone.
fatal: Remote branch 0.1.0 not found in upstream origin
) during validation.
Analyzed 1 podspec.
[!] The spec did not pass validation, due to 1 error.

根据提示,我们需要先建立一个 Tag:

1
2
3
git tag '0.1.0'
git push --tags
pod spec lint

检验通过后,提示如下:

1
2
3
4
5
-> BaseComponent (0.1.0)
Analyzed 1 podspec.
BaseComponent.podspec passed validation.

然后将 podspec 文件推到远程私有索引库:

1
pod repo push TZYSpecs BaseComponent.podspec

现在看一下本地索引库中是否已经添加成功:

1
~/.cocoapods/repos

再看一看你的远程索引库中是否添加成功,现在搜索一下本地索引文件试试:

1
2
3
4
5
6
-> BaseComponent (0.1.0)
基础组价
pod 'BaseComponent', '~> 0.1.0'
- Homepage: https://coding.net/u/tianziyao/p/BaseComponent
- Source: https://git.coding.net/tianziyao/BaseComponent.git
- Versions: 0.1.0 [TZYSpecs repo]

现在我们可以找到自己的远程私有库了,下面将 Podfile 文件改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
project 'Ting.xcodeproj'
source 'https://github.com/CocoaPods/Specs.git'
source 'git@git.coding.net:tianziyao/TZYSpecs.git'
target 'Ting' do
use_frameworks!
pod 'BaseComponent'
pod 'Alamofire'
target 'TingTests' do
inherit! :search_paths
end
target 'TingUITests' do
inherit! :search_paths
end
end

执行 pod install,整个远程私有库的搭建和使用就完成了。

CocoaPods 库升级

我们使用远程私有库的目的就是为了版本升级和多人开发,那么远程私有库如何进行升级,升级后其他人又如何使用呢?现在我们给 BaseComponent 进行升级,给它再增加一些功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── BaseComponent
│   ├── Assets
│   └── Classes
│   ├── Const
│   │   └── Const.swift
│   ├── Extension
│   │   ├── Array+Safe.swift
│   │   ├── CALayer+PauseAimate.swift
│   │   ├── UIImage+.swift
│   │   └── UIView+Property.swift
│   └── Tool
│   ├── AlertTool.swift
│   ├── CacheTool.swift
│   ├── DeviceMessage.swift
│   └── NoticeLocalTool.swift
├── BaseComponent.podspec
├── Example
├── LICENSE
├── README.md
└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj

BaseComponent 的测试工程中测试无误后,将 BaseComponent.podspecversion 修改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Pod::Spec.new do |s|
s.name = 'BaseComponent'
s.version = '0.2.0'
s.summary = '基础组价'
s.description = '包括基本配置,常量,扩展,工具类等'
s.homepage = 'https://coding.net/u/tianziyao/p/BaseComponent'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'tianziyao' => 'ziyao.tian@gmail.com' }
s.source = { :git => 'https://git.coding.net/tianziyao/BaseComponent.git', :tag => s.version.to_s }
s.ios.deployment_target = '8.0'
s.source_files = 'BaseComponent/Classes/**/*'
end

现在检查一下私有库是否有错误:

1
pod lib lint

检查通过后就可以将 BaseComponent0.2.0 版本推到远程私有库中,同时建立 0.2.0 的 Tag。

然后检查一下 spec 文件:

1
pod spec lint

检查通过后,执行:

1
pod repo push TZYSpecs BaseComponent.podspec

远程私有库和远程私有索引库全部更新完毕,现在我们回到使用者的视角,这个库可以使用了吗?还不行。

因为本地的索引文件还没有更新,这个源还找不到,现在进入壳工程,执行:

1
2
pod update --no-repo-update
pod install

BaseComponent0.2.0 版本就乖乖的进入了壳工程。

CocoaPods 库依赖

在上面的壳工程中,我们引入了 Alamofire 这个框架,但是如果用着用着突然觉得不爽了,要换框架,这时 Alamofire 的引用在工程中已经无处不再了,这样换的话是不是很痛苦?

所以我们一般在开发中都会封装网络请求,做到分层解耦,这样如果换框架,只修改网络请求这层的封装就可以了,那么现在我们需要将 Alamofire 封装成 Network,再把 Network 弄到我们的 BaseComponent 里面去,怎么做呢?

现在先将 Network 拖到 BaseComponentClasses 目录中,因为 BaseComponent 的测试工程没有 Alamofire,所以 Network 肯定是会报错了,不要慌,下面我们修改 spec 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Pod::Spec.new do |s|
s.name = 'BaseComponent'
s.version = '0.2.0'
s.summary = '基础组价'
s.description = '包括基本配置,常量,扩展,工具类等'
s.homepage = 'https://coding.net/u/tianziyao/p/BaseComponent'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'tianziyao' => 'ziyao.tian@gmail.com' }
s.source = { :git => 'https://git.coding.net/tianziyao/BaseComponent.git', :tag => s.version.to_s }
s.ios.deployment_target = '8.0'
s.source_files = 'BaseComponent/Classes/**/*'
s.dependency 'Alamofire'
s.dependency 'SDWebImage'
end

dependency 指明了这个库的依赖,改好之后 pod installAlamofire 就安装到了 BaseComponent 的测试工程中,现在就可以使用 Alamofire 进行网络请求封装,直接 import 就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation
import Alamofire
import SDWebImage
open class Network {
open class func request(url: String, parameters: [String:Any]?) {
Alamofire.request(url, method: .get, parameters: parameters).responseJSON { (response) in
guard let JSON = response.result.value else { return }
print(JSON)
}
}
}
extension UIImageView {
public func image(with url: URL?) {
self.sd_setImage(with: url)
}
}

现在再进行一次远程私有库升级,整个依赖就做好了,需要注意的是,已经做了依赖的话,相关的库就可以从 Podfile 文件中去掉了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
project 'Ting.xcodeproj'
source 'https://github.com/CocoaPods/Specs.git'
source 'git@git.coding.net:tianziyao/TZYSpecs.git'
target 'Ting' do
use_frameworks!
pod 'BaseComponent'
# pod 'Alamofire'
target 'TingTests' do
inherit! :search_paths
end
target 'TingUITests' do
inherit! :search_paths
end
end

现在是我们依赖的是公开库,直接升级 CocoaPods 私有库就可以,但是如果依赖的是另外一个私有库,这个依赖关系最终还要上传到私有索引库中,这样其他人在使用的时候才会知道这个依赖关系,现在走一下升级的流程,你会得到类似这个报错:

1
[!] The `TargetComponent.podspec` specification does not validate.

这个报错是因为 TargetComponent 这个库没有在官方的索引库当中,忽略就可以了,当然,在使用的时候,TargetComponent 这个库可以在你的本地索引文件中找到,否则无法使用。

CocoaPods 资源依赖

现在我们可以让一个库依赖另外一个库,但是看下面这段代码:

1
2
3
4
5
/// 获取中间的视图
open class func tabBarMiddleView() -> TZYTabBarMiddleView {
let view = Bundle.main.loadNibNamed("TZYTabBarMiddleView", owner: nil, options: nil)?.first
return (view as? TZYTabBarMiddleView) ?? TZYTabBarMiddleView()
}

这段代码读取了一个 XIB 文件,这个库的结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── Assets
└── Classes
└── MainModule
├── Controller
│   ├── TZYNavBarC.swift
│   └── TZYTabBarC.swift
└── View
├── TZYNavBar.swift
├── TZYTabBar.swift
├── TZYTabBarMiddleView.swift
└── TZYTabBarMiddleView.xib

我们可以成功调用这个方法吗?不能,因为 TZYTabBarMiddleView.xib 这个文件的 Target 是 MainModule,使用 CocoaPods 把这个库安装到我们项目后,XIB 文件即使在,也是在 Pods 这个工程里,而我们在壳工程中使用 TZYTabBarMiddleView.xib,也是必然找不到的。

下面我们把模板库的测试工程编译一下,打开 Products 目录下的 .app 文件,看一下文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.
├── Base.lproj
│   ├── LaunchScreen.nib
│   └── Main.storyboardc
├── Frameworks
│   ├── Alamofire.framework
│   │   ├── Alamofire
│   │   ├── Info.plist
│   │   └── _CodeSignature
│   │   └── CodeResources
│   ├── BaseComponent.framework
│   │   ├── BaseComponent
│   │   ├── Info.plist
│   │   └── _CodeSignature
│   │   └── CodeResources
│   ├── SDWebImage.framework
│   │   ├── Info.plist
│   │   ├── SDWebImage
│   │   └── _CodeSignature
│   │   └── CodeResources
│   ├── TargetComponent.framework
│   │   ├── Info.plist
│   │   ├── TZYTabBarMiddleView.nib
│   │   ├── TargetComponent
│   │   └── _CodeSignature
│   │   └── CodeResources
├── Info.plist
├── PkgInfo
├── TargetComponent_Example
├── _CodeSignature
│   └── CodeResources
└── libswiftRemoteMirror.dylib

通过路径可以看到,TZYTabBarMiddleView.nib 是在:

1
mainBundle/Frameworks/TargetComponent.framework

这个路径下面,因此 mainBundle.loadXIb 肯定是找不到资源文件的,那么该如何修改呢?

1
2
3
4
5
open class func tabBarMiddleView() -> TZYTabBarMiddleView {
let view = Bundle(for: TZYTabBarMiddleView.self).loadNibNamed("TZYTabBarMiddleView", owner: nil, options: nil)?.first
//let view = Bundle.main.loadNibNamed("TZYTabBarMiddleView", owner: nil, options: nil)?.first
return (view as? TZYTabBarMiddleView) ?? TZYTabBarMiddleView()
}

这部分的重点就是 Bundle(for aClass: Swift.AnyClass) 这个方法。

CocoaPods 图片依赖

上面我们讲到了怎样使用 Pod 库里面的 XIB 文件,但是还有其他资源文件,例如图片、音频、视频,图片我们一般是放在 Assets.xcassets,但是 Pod 库并没有对应的路径,那么它所需要的图片放在哪里,已经如何使用呢?现在使用 pod lib create 命令创建一个 Pod 库,进入以下路径:

1
组件名/Assets

把一些图片拖入到 Assets 文件夹内,然后在 podspec 文件中加入以下代码:

1
2
3
4
s.resource_bundles = {
'组件名' => ['组件名/Assets/*.png'] //只加载 png 文件
# '组件名' => ['组件名/Assets/*'] //加载所有文件
}

然后执行 pod install,Pod 库中就出现了之前拖入 Assets 文件夹的图片,但是现在还不能使用,我们先来看一下打包以后这些图片的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.
├── Base.lproj
│   ├── LaunchScreen.nib
│   └── Main.storyboardc
│   ├── Info.plist
│   ├── UIViewController-vXZ-lx-hvc.nib
│   └── vXZ-lx-hvc-view-kh9-bI-dsS.nib
├── Frameworks
│   ├── TargetComponent.framework
│   │   ├── Info.plist
│   │   ├── TZYTabBarMiddleView.nib
│   │   ├── TargetComponent
│   │   ├── TargetComponent.bundle
│   │   │   ├── Info.plist
│   │   │   ├── tabbar_bg_320x49_@3x.png
│   │   │   └── zxy_icon_48x48_@2x.png
│   │   └── _CodeSignature
│   │   └── CodeResources
│   └── libswiftUIKit.dylib
├── Info.plist
├── PkgInfo
├── TargetComponent_Example
├── _CodeSignature
│   └── CodeResources
├── embedded.mobileprovision
└── libswiftRemoteMirror.dylib

可以看到,打包后的路径在:

1
mainBundle/Frameworks/TargetComponent.framework/TargetComponent.bundle

这个路径下面,而代码中的 UIImage(named: "tabbar_bg") 读取的是 mainBundle 下的资源文件,因此还是找不到的,那么这时使用图片,应该将代码改成这样:

1
2
3
4
5
6
7
8
9
10
11
backgroundImage = UIImage.image(withName: "tabbar_bg_320x49_@3x.png")
extension UIImage {
public class func image(withName name: String) -> UIImage? {
let bundle = Bundle(for: UIImage.self)
guard let path = bundle.path(forResource: name, ofType: nil, inDirectory: "TargetComponent.bundle") else {
return nil
}
return UIImage(contentsOfFile: path)
}
}

这里需要注意的是,文件名需要完整。以上是在代码中加载图片,如果是在 XIB 中加载图片,应该怎样做呢?那么再看一下上面的目录结构,TZYTabBarMiddleView.nibTargetComponent.bundle 处于同一个目录,我们可以在 TZYTabBarMiddleView.xib 中通过相对路径,使用 TargetComponent.bundle 里面的图片,因此在 XIB 中,图片名应该是这样的:

1
TargetComponent.bundle/tabbar_bg_320x49_@3x

CocoaPods 子库

现在我们实现了一个完整的远程私有库,可以升级,依赖其他的库,提供给其他人使用,但是现在还有一点问题,其他人如果要用我们的库,就需要把 BaseComponent 完整的克隆过来,但是他可能只需要 BaseComponent 里面的 Network,其他的扩展、工具等并不想使用,也不想导入过来,怎么办?有两种方案:

  1. Network 剥离出来,再单独建一个远程私有库;
  2. 使用子库迁出 Network

第一种方案大家已经知道了,就是上面的一大篇,麻烦不说,而且东西一多起来,这里一个库,那里一个库,也不容易管理,所以,下面就有请子库隆重登场。

在开始之前,我们先来开一个东西,下面是 pod search AFN 中的一条记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-> AFNetworking (3.1.0)
A delightful iOS and OS X networking framework.
pod 'AFNetworking', '~> 3.1.0'
- Homepage: https://github.com/AFNetworking/AFNetworking
- Source: https://github.com/AFNetworking/AFNetworking.git
- Versions: 3.1.0, 3.0.4, 3.0.3, 3.0.2, 3.0.1, 3.0.0, 3.0.0-beta.3, 3.0.0-beta.2, 3.0.0-beta.1, 2.6.3, 2.6.2, 2.6.1, 2.6.0, 2.5.4, 2.5.3, 2.5.2, 2.5.1, 2.5.0, 2.4.1,
2.4.0, 2.3.1, 2.3.0, 2.2.4, 2.2.3, 2.2.2, 2.2.1, 2.2.0, 2.1.0, 2.0.3, 2.0.2, 2.0.1, 2.0.0, 2.0.0-RC3, 2.0.0-RC2, 2.0.0-RC1, 1.3.4, 1.3.3, 1.3.2, 1.3.1, 1.3.0, 1.2.1,
1.2.0, 1.1.0, 1.0.1, 1.0, 1.0RC3, 1.0RC2, 1.0RC1, 0.10.1, 0.10.0, 0.9.2, 0.9.1, 0.9.0, 0.7.0, 0.5.1 [master repo]
- Subspecs:
- AFNetworking/Serialization (3.1.0)
- AFNetworking/Security (3.1.0)
- AFNetworking/Reachability (3.1.0)
- AFNetworking/NSURLSession (3.1.0)
- AFNetworking/UIKit (3.1.0)

注意 Subspecs 这里,它就是本节要讲的东西,首先将 spec 改成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Pod::Spec.new do |s|
s.name = 'BaseComponent'
s.version = '0.4.0'
s.summary = '基础组价'
s.description = '包括基本配置,常量,扩展,工具类等'
s.homepage = 'https://coding.net/u/tianziyao/p/BaseComponent'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'tianziyao' => 'ziyao.tian@gmail.com' }
s.source = { :git => 'https://git.coding.net/tianziyao/BaseComponent.git', :tag => s.version.to_s }
s.ios.deployment_target = '8.0'
# s.source_files = 'BaseComponent/Classes/**/*'
s.subspec 'Network' do |n|
n.source_files = 'BaseComponent/Classes/Network/**/*'
n.dependency 'Alamofire'
n.dependency 'SDWebImage'
end
s.subspec 'Const' do |c|
c.source_files = 'BaseComponent/Classes/Const/**/*'
end
s.subspec 'Extension' do |e|
e.source_files = 'BaseComponent/Classes/Extension/**/*'
end
s.subspec 'Tool' do |t|
t.source_files = 'BaseComponent/Classes/Tool/**/*'
end
end

在这里要注意 source_filesdependency 以及版本的变化,修改完成推到远程索引库,并打好 0.4.0 的分支,执行:

1
2
3
4
pod spec lint
pod repo push TZYSpecs BaseComponent.podspec
pod update --no-repo-update
pod search Base

现在就可以找到了:

1
2
3
4
5
6
7
8
9
10
11
-> BaseComponent (0.4.0)
基础组价
pod 'BaseComponent', '~> 0.4.0'
- Homepage: https://coding.net/u/tianziyao/p/BaseComponent
- Source: https://git.coding.net/tianziyao/BaseComponent.git
- Versions: 0.4.0, 0.3.0, 0.2.0, 0.1.0 [TZYSpecs repo]
- Subspecs:
- BaseComponent/Network (0.4.0)
- BaseComponent/Const (0.4.0)
- BaseComponent/Extension (0.4.0)
- BaseComponent/Tool (0.4.0)

那么如何使用呢?把 Podfile 改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
project 'Ting.xcodeproj'
source 'https://github.com/CocoaPods/Specs.git'
source 'git@git.coding.net:tianziyao/TZYSpecs.git'
target 'Ting' do
use_frameworks!
pod 'BaseComponent/Network'
# 也可以用下面的写法
# pod 'BaseComponent', :subspecs => ['Network', 'Extension']
target 'TingTests' do
inherit! :search_paths
end
target 'TingUITests' do
inherit! :search_paths
end
end

现在 pod install,就完成了子库的创建和使用。

结尾

这篇文章断断续续写了快一周,其实一般我们用不到 CocoaPods 这些功能,不过了解一下 CocoaPods 的工作原理也是没有坏处的。

这篇文章主要是为了使用 CocoaPods 进行组件化开发,关于组件化开发的思想,可以看下面这篇文章:

iOS 组件化实践思考